/*
 * Classe do PEER RENDEZVOUS
 */
package trabalho3sd;

import java.io.File;
import java.io.IOException;
import net.jxta.document.AdvertisementFactory;
import net.jxta.endpoint.Message;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.IDFactory;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.peergroup.PeerGroupID;
import net.jxta.pipe.PipeMsgEvent;
import net.jxta.pipe.PipeMsgListener;
import net.jxta.pipe.PipeService;
import net.jxta.platform.NetworkConfigurator;
import net.jxta.platform.NetworkManager;
import net.jxta.protocol.PipeAdvertisement;
import net.jxta.util.JxtaServerPipe;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jxta.util.JxtaBiDiPipe;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import net.jxta.endpoint.StringMessageElement;

/**
 * Classe Rendezvous
 *
 * @author Gustavo Luvizotto Cesar - 6783544 - gustavoluvizotto@gmail.com
 * @author Leonardo Lourenço Crespilho - 5124310 - lcrespilho@gmail.com
 * @author Murilo Alencar Alves Júnior - muriloaajruspec@gmail.com
 * @link https://code.google.com/p/sd0642-t3-g3/
 */
public class Rendezvous implements PipeMsgListener {

    private static final String PipeType = PipeService.UnicastType;     // pipe criado do tipo unicas
    private static final String Name = "Rendezvous";                    // nome para esse peer
    public static final int TcpPort = 9722;                             // porta em que ele irá se comunicar
    private static final PeerID rdvPeerID = IDFactory.newPeerID(PeerGroupID.defaultNetPeerGroupID, Name.getBytes());    // seu PID
    private static final File ConfigurationFile = new File("." + System.getProperty("file.separator") + Name);          // arquivos de configuração da rede   
    private JxtaBiDiPipe MyBiDiPipe;
    private static JxtaBiDiPipe MyBiDiPipeAux;
    private static List<Metadado> MetadadoList = new ArrayList<>(); // Lista de metadados referentes aos arquivos compartilhados
    private static PeerGroup NetPeerGroup;

    /**
     * Construtor da classe Rendezvous
     *
     * @param pipe
     */
    public Rendezvous(JxtaBiDiPipe pipe) {
        this.MyBiDiPipe = pipe;
    }

    /**
     * Método listener de mensagens que chegam no BiDiPipe
     * <p>A mensagem pode ser dos seguintes tipos:
     * <p>- LISTARMETADADOS: imprime na tela (do RDV) a lista de metadados.\
     * <p>- PUBLICAR: quando o peer deseja compartilhar um arquivo que ele possui
     * <p>- DELETAR: quando o peer deseja deletar um arquivo compartilhado
     * <p>- DELETAMETADADO: quando o peer dá exit(), avisa pro RDV remover os metadados dos arquivos que ele possui.
     * <p>- ACESSAR: quando o peer deseja usar um arquivo compartilhado
     * <p>- TERMINOACESSO: indica que o peer terminou de usar o arquivo
     */
    @Override
    public void pipeMsgEvent(PipeMsgEvent PME) {    // método para manipulador eventos para as mensagens que chegam. Esse cara enviará mensagens também

        try {
            // foi recebida uma mensagem. devemos tratá-la pra saber se é um pedido de recurso ou não
            Message ReceivedMessage = PME.getMessage();
            String msgType = ReceivedMessage.getMessageElement("TYPE").toString();                              //pode ser publicar ou acessar
            String msgFileName = ReceivedMessage.getMessageElement("FILENAME").toString();                      //nome do arquivo a ser publicado ou acessado/baixado
            long msgLastModified = Long.parseLong(ReceivedMessage.getMessageElement("LASTMODIFIED").toString());
            PeerID remotePeerID = MyBiDiPipe.getRemotePeerAdvertisement().getPeerID();                          //PeerID do peer que mandou essa mensagem

            switch (msgType) {

                case "LISTARMETADADOS": {
                    Iterator<Metadado> it = MetadadoList.iterator();
                    System.out.println("LISTAGEM DOS METADADOS:");
                    System.out.println("---------------------------------------");
                    while (it.hasNext()) {
                        Metadado Meta = it.next();
                        System.out.println("Arquivo: " + Meta.getFileName() + " | " + Meta.getState() + " | Data: " + Meta.getLastModification());
                        System.out.println("PeerID: " + Meta.getPeerID().toString());
                        System.out.println("BiDiPipeSocket: " + Meta.getRDV2PeerBiDiPipeSocket().toString());
                        System.out.println("");
                    }
                    break;
                }

                case "PUBLICAR": {
                    System.out.println("Recebi uma msg PUBLICAR, com as seguintes informações:");
                    System.out.println("msgFileName: " + msgFileName);
                    System.out.println("msgLastModified: " + msgLastModified);
                    System.out.println("PeerID do sender: " + remotePeerID.toString());
                    System.out.flush();
                    boolean new_metadado_flag = true;
                    Iterator<Metadado> it = MetadadoList.iterator();
                    while (it.hasNext()) {
                        Metadado Meta = it.next();

                        /* IMPORTANTE: não tratamos o caso dum metadado estar executando. O Peer deve realizar
                         * TERMINOACESSO pra um arquivo em execução. Qualquer tentativa de publicação pra um arquivo
                         * que está sendo usado não deve ser aceita.
                         */
                        if (Meta.getState().equals("EXECUTANDO")) {
                            continue;   // analisa o próximo metadado
                        }

                        /* Caso em que um peer está tentando publicar a mesma versão do arquivo que ELE MESMO já publicou anteriormente:
                         * nesse caso o RDV não faz nada, já que o metadado já existe. */
                        if (Meta.getFileName().equals(msgFileName) && Meta.getLastModification() == msgLastModified && Meta.getPeerID().equals(remotePeerID)) {
                            System.out.println("Esse peer já publicou esse mesmo arquivo e com a mesma versão. Não tenho que fazer nada...");
                            System.out.flush();
                            new_metadado_flag = false;
                            break;
                        }

                        /* Caso em que um peer está tentando publicar uma versão igual ou mais antiga de um arquivo já publicado por OUTRO peer:
                         * nesse caso o RDV deve mandar esse peer deletar sua cópia do arquivo, pois ela é antiga, ou já foi publicada por outro peer
                         */
                        if (Meta.getFileName().equals(msgFileName) && !Meta.getPeerID().equals(remotePeerID) && Meta.getLastModification() > msgLastModified) {
                            System.out.println("Manda o peer " + remotePeerID + " deletar sua cópia do arquivo " + msgFileName);
                            System.out.flush();
                            msgDELETAR(msgFileName, MyBiDiPipe);
                            new_metadado_flag = false;
                            break;
                        }

                        /* Caso em que qualquer peer está publicando um arquivo já publicado, porém mais atual:
                         * nesse caso o RDV aceita a publicação. Além disso, se o owner do arquivo antigo for diferente do peer
                         * que está publicando agora, manda o peer antigo deletar sua cópia desatualizada do arquivo. 
                         */
                        if (Meta.getFileName().equals(msgFileName) && msgLastModified > Meta.getLastModification()) {
                            if (!Meta.getPeerID().equals(remotePeerID)) {
                                System.out.println("Manda o peer " + Meta.getPeerID() + " deletar sua cópia (antiga) do arquivo " + msgFileName);
                                System.out.flush();
                                msgDELETAR(msgFileName, Meta.getRDV2PeerBiDiPipeSocket());
                            }
                            System.out.println("O peer " + remotePeerID + " está inserindo uma versão mais nova do metadado " + msgFileName);
                            System.out.flush();
                            modificaMetadado(Meta, msgFileName, msgLastModified, remotePeerID);
                            new_metadado_flag = false;
                            break;
                        }
                        
                        
                        if (Meta.getFileName().equals(msgFileName) && msgLastModified < Meta.getLastModification() && Meta.getPeerID().equals(remotePeerID)) {
                            System.out.println("Violação das regras do sistema. Um arquivo no diretório /tmp/JXTA só pode ser acessado/alterado,"
                                    + " se comunicado ao sistema via menu '[3]-Acessa/Baixa um arquivo'.");
                            new_metadado_flag = false;
                            break;
                        }
                    }

                    /* Caso em que o metadado sendo inserido é novo:
                     * nesse caso adicionamos o metadado. 
                     */
                    if (new_metadado_flag) {
                        System.out.println("Peer " + remotePeerID + " está inserindo um novo metadado: " + msgFileName);
                        System.out.flush();
                        insereMetadado(msgFileName, msgLastModified, remotePeerID);
                    }
                    System.out.println("");
                    break;
                }

                case "DELETAR": {
                    Metadado Meta;
                    Message MyMessage = new Message();
                    Iterator<Metadado> it = MetadadoList.iterator();    // MetadadoList é uma lista de Metadado

                    while (it.hasNext()) {                              // teoricamente tem no máximo 1 metadado com o arquivo procurado
                        Meta = it.next();
                        if (Meta.getFileName().equals(msgFileName)) {
                            MyMessage.addMessageElement(new StringMessageElement("TYPE", "DELETAR", null));
                            MyMessage.addMessageElement(new StringMessageElement("FILENAME", Meta.getFileName(), null));
                            MyMessage.addMessageElement(new StringMessageElement("PEERID", "lixo", null));
                            Meta.getRDV2PeerBiDiPipeSocket().sendMessage(MyMessage);
                            System.out.println("Mandando o peer deletar o arquivo " + msgFileName + " e deletando o metadado correspondente.");
                            System.out.println("");
                            System.out.flush();
                            it.remove();
                        }
                    }
                    break;
                }

                case "DELETAMETADADO": {
                    Metadado Meta;
                    Iterator<Metadado> it = MetadadoList.iterator();    // MetadadoList é uma lista de Metadado

                    while (it.hasNext()) {      // teoricamente tem no máximo 1 metadado com o arquivo procurado
                        Meta = it.next();
                        if (Meta.getFileName().equals(msgFileName)) {
                            System.out.println("Deletando o metadado: " + msgFileName);
                            System.out.println("");
                            System.out.flush();
                            it.remove();
                            break;
                        }
                    }
                    break;
                }

                case "ACESSAR": {
                    boolean notfound_or_executando = true;
                    Message MyMessage = new Message();
                    Iterator<Metadado> it = MetadadoList.iterator();

                    while (it.hasNext()) {
                        Metadado Meta = it.next();

                        /* se arquivo existe, está DISPONIVEL e está no peer solicitate:
                         * nesse caso não precisamos abrir sockets. 
                         */
                        if (Meta.getFileName().equals(msgFileName) && Meta.getState().equals("DISPONIVEL") && Meta.getPeerID().equals(remotePeerID)) {
                            notfound_or_executando = false;
                            Meta.setState("EXECUTANDO");
                            MyMessage.addMessageElement(new StringMessageElement("FILENAME", msgFileName, null));
                            MyMessage.addMessageElement(new StringMessageElement("TYPE", "DOWNLOAD", null));
                            MyMessage.addMessageElement(new StringMessageElement("PEERID", Meta.getPeerID().toString(), null));
                            MyBiDiPipe.sendMessage(MyMessage);
                            break;
                        }

                        /* se arquivo existe, está DISPONIVEL e está em outro peer:
                         * nesse caso devemos usar sockets 
                         */
                        if (Meta.getFileName().equals(msgFileName) && Meta.getState().equals("DISPONIVEL") && !Meta.getPeerID().equals(remotePeerID)) {
                            notfound_or_executando = false;
                            String peerA = remotePeerID.toString();
                            String peerB = Meta.getPeerID().toString();
                            JxtaBiDiPipe BiDiPipeComPeerA = MyBiDiPipe;
                            JxtaBiDiPipe BiDiPipeComPeerB = Meta.getRDV2PeerBiDiPipeSocket();

                            /* Avisa peerB que ele deve abrir um socket */
                            MyMessage.addMessageElement(new StringMessageElement("TYPE", "ABRESOCKET", null));
                            MyMessage.addMessageElement(new StringMessageElement("FILENAME", msgFileName, null));
                            MyMessage.addMessageElement(new StringMessageElement("PEERID", "lixo", null));
                            System.out.println("Mandando mensagem ABRESOCKET para o peer " + peerB);
                            System.out.flush();
                            BiDiPipeComPeerB.sendMessage(MyMessage);

                            /* Altera as informações do metadado do arquivo:
                             * Agora o arquivo pertence ao peerA, e não mais ao peerB. 
                             */
                            Meta.setState("EXECUTANDO");
                            Meta.setPeerID(remotePeerID);
                            Meta.setRDV2PeerBiDiPipeSocket(BiDiPipeComPeerA);

                            /* Avisa peerA que ele deve fazer download do arquivo de peerB */
                            MyMessage = new Message();
                            MyMessage.addMessageElement(new StringMessageElement("TYPE", "DOWNLOAD", null));
                            MyMessage.addMessageElement(new StringMessageElement("FILENAME", msgFileName, null));
                            MyMessage.addMessageElement(new StringMessageElement("PEERID", peerB, null));
                            System.out.println("Mandando a mensagem de DOWNLOAD pro peer solicitante " + peerA);
                            System.out.flush();
                            BiDiPipeComPeerA.sendMessage(MyMessage);
                        }
                    }

                    if (notfound_or_executando) {  // arquivo não encontrado ou STATE=EXECUTANDO, avisamos o peer que o solicitou
                        MyMessage.addMessageElement(new StringMessageElement("TYPE", "ACESSOERRO", null));
                        MyMessage.addMessageElement(new StringMessageElement("FILENAME", "lixo", null));
                        MyMessage.addMessageElement(new StringMessageElement("PEERID", "lixo", null));
                        System.out.println("Mandando mensagem de ACESSOERRO para o peer solicitante (" + remotePeerID.toString() + "). Arquivo não existe ou STATUS=EXECUTANDO.");
                        System.out.flush();
                        MyBiDiPipe.sendMessage(MyMessage);
                    }
                    System.out.println("");
                    break;
                }

                /* Usado para indicar quando um peer terminou de acessar um arquivo, ou seja,
                 * o status do arquivo tem que mudar de EXECUTANDO para DISPONIVEL */
                case "TERMINOACESSO": {
                    Iterator<Metadado> it = MetadadoList.iterator();
                    Metadado Meta;

                    while (it.hasNext()) {
                        Meta = it.next();
                        if (Meta.getFileName().equals(msgFileName) && Meta.getPeerID().equals(remotePeerID) && msgLastModified >= Meta.getLastModification() && Meta.getState().equals("EXECUTANDO")) {
                            System.out.println("Recebi mensagem TERMINOACESSO para o arquivo " + msgFileName);
                            System.out.println("Setando STATUS=DISPONIVEL e LASTMODIFICATION para a data recebida " + msgLastModified);
                            System.out.flush();
                            Meta.setState("DISPONIVEL");
                            Meta.setLastModification(msgLastModified);
                        }
                    }
                    System.out.println("");
                    break;
                }
            }
        } catch (IOException ex) {
            Logger.getLogger(Rendezvous.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     *
     * @return
     */
    public static PipeAdvertisement getPipeAdvertisement() {
        // Aqui criamos um Ad para o pipe de comunicação. Criamos o pipe através da AdvertisementFactory, pegamos a sua ID e setamos seus parametros
        PipeAdvertisement MyPipeAdvertisement = (PipeAdvertisement) AdvertisementFactory.newAdvertisement(PipeAdvertisement.getAdvertisementType());
        MyPipeAdvertisement.setPipeID(IDFactory.newPipeID(PeerGroupID.defaultNetPeerGroupID, Name.getBytes()));
        MyPipeAdvertisement.setType(PipeType);
        MyPipeAdvertisement.setName("Advertisement do BiDiPipe");
        MyPipeAdvertisement.setDescription("Created by " + Name);
        return MyPipeAdvertisement;
    }

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        try {
            Logger.getLogger("net.jxta").setLevel(Level.SEVERE);
            System.out.println("Removendo diretório de cache jxta anterior");
            NetworkManager.RecursiveDelete(ConfigurationFile);  // eliminando configurações da rede anteriores

            // Criação de uma NetworkManager
            NetworkManager MyNetworkManager = new NetworkManager(NetworkManager.ConfigMode.RENDEZVOUS, Name, ConfigurationFile.toURI());

            // Recuperando as configurações da network
            NetworkConfigurator MyNetworkConfigurator = MyNetworkManager.getConfigurator();

            // ajustando algumas configurações
            MyNetworkConfigurator.setTcpPort(TcpPort);
            MyNetworkConfigurator.setTcpEnabled(true);
            MyNetworkConfigurator.setTcpIncoming(true);
            MyNetworkConfigurator.setTcpOutgoing(true);
            MyNetworkConfigurator.setUseMulticast(false);       // desabilitamos multicast para poder "criar" o nó central
            MyNetworkConfigurator.setPeerID(rdvPeerID);
            NetPeerGroup = MyNetworkManager.startNetwork();

            // Cria o socket servidor para Pipe Bidirecional setando o timeout (para o accept()) de 30 min. Backlog = 10 conexões na fila de espera
            JxtaServerPipe MyBiDiPipeServer = new JxtaServerPipe(NetPeerGroup, getPipeAdvertisement(), 10, 30 * 60 * 1000);

            /* Publica o advertisement do meu (do RDV) Pipe Bidirecional. Os EDGEs que quiserem conectar-se nele devem fazê-lo
             * através desse Pipe Advertisement.
             */
            NetPeerGroup.getDiscoveryService().publish(getPipeAdvertisement()); // expira em 2h por padrão

            while (true) {  // Cria conexão com o BiDiPipe e associa a este novo socket o Listener correspondente
                MyBiDiPipeAux = MyBiDiPipeServer.accept();
                listaPeersConectados();
                if (MyBiDiPipeAux != null) {
                    MyBiDiPipeAux.setMessageListener(new Rendezvous(MyBiDiPipeAux));
                }
            }
        } catch (PeerGroupException | IOException ex) {
            Logger.getLogger(Rendezvous.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.exit(0);
    }

    public static void listaPeersConectados() {
        List<PeerID> PIDList = NetPeerGroup.getRendezVousService().getLocalEdgeView();
        Iterator<PeerID> it = PIDList.iterator();
        while (it.hasNext()) {
            System.out.println("Peer conectado no RDV: " + it.next().toString());
        }
        System.out.println("\n");
    }

    /*
     * Método usado para inserir os metadados recebidos por mensagens na lista de metadados
     */
    void insereMetadado(String msgFileName, long msgLastModified, PeerID remotePeerID) {
        Metadado Meta = new Metadado();

        Meta.setFileName(msgFileName);
        Meta.setLastModification(msgLastModified);
        Meta.setState("DISPONIVEL");
        Meta.setPeerID(remotePeerID);
        Meta.setRDV2PeerBiDiPipeSocket(this.MyBiDiPipe);
        MetadadoList.add(Meta); // adiciona o elemento na lista
    }

    /*
     * Método usado para modificar os metadados apenas com dados atuais
     */
    void modificaMetadado(Metadado Meta, String msgFileName, long msgLastModified, PeerID remotePeerID) {
        Meta.setFileName(msgFileName);
        Meta.setLastModification(msgLastModified);
        Meta.setState("DISPONIVEL");
        Meta.setPeerID(remotePeerID);
        Meta.setRDV2PeerBiDiPipeSocket(this.MyBiDiPipe);
    }

    /* Tarefa desse método: Enviar uma mensagem pro peer remoto, dizendo pra ele simplesmente deletar sua
     * cópia do arquivo. No peer, a consequência disso é simplesmente deletar o arquivo, sem mandar confirmação
     * nem nada mais. O peer dá um file.delete("caminho do arquivo") e nada mais.
     */
    void msgDELETAR(String fileName, JxtaBiDiPipe RDV2PeerBiDiPipeSocket) {
        try {
            Message MyMessage = new Message();
            MyMessage.addMessageElement(new StringMessageElement("TYPE", "DELETAR", null));
            MyMessage.addMessageElement(new StringMessageElement("FILENAME", fileName, null));
            MyMessage.addMessageElement(new StringMessageElement("PEERID", "lixo", null));
            RDV2PeerBiDiPipeSocket.sendMessage(MyMessage);
        } catch (IOException ex) {
            Logger.getLogger(Rendezvous.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
